arize-phoenix 4.28.1__py3-none-any.whl → 4.30.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (29) hide show
  1. {arize_phoenix-4.28.1.dist-info → arize_phoenix-4.30.0.dist-info}/METADATA +2 -1
  2. {arize_phoenix-4.28.1.dist-info → arize_phoenix-4.30.0.dist-info}/RECORD +26 -22
  3. phoenix/otel/__init__.py +22 -0
  4. phoenix/otel/otel.py +284 -0
  5. phoenix/otel/settings.py +82 -0
  6. phoenix/server/api/dataloaders/dataset_example_revisions.py +3 -2
  7. phoenix/server/api/exceptions.py +41 -0
  8. phoenix/server/api/mutations/api_key_mutations.py +29 -0
  9. phoenix/server/api/mutations/auth_mutations.py +4 -2
  10. phoenix/server/api/mutations/dataset_mutations.py +9 -8
  11. phoenix/server/api/mutations/experiment_mutations.py +2 -1
  12. phoenix/server/api/queries.py +9 -8
  13. phoenix/server/api/routers/v1/experiments.py +4 -4
  14. phoenix/server/api/schema.py +2 -0
  15. phoenix/server/static/.vite/manifest.json +31 -31
  16. phoenix/server/static/assets/components-CkSg5zK4.js +1344 -0
  17. phoenix/server/static/assets/{index-fqdjNpYm.js → index-DTecsU5w.js} +1 -1
  18. phoenix/server/static/assets/{pages-DnbxgoTK.js → pages-C6emDFIO.js} +212 -210
  19. phoenix/server/static/assets/vendor-DsnEJuEV.js +641 -0
  20. phoenix/server/static/assets/{vendor-arizeai-CsdcB1NH.js → vendor-arizeai-DtynTLNi.js} +2 -2
  21. phoenix/server/static/assets/vendor-codemirror-C5to5cK4.js +27 -0
  22. phoenix/server/static/assets/{vendor-recharts-B0sannek.js → vendor-recharts-reihe2SJ.js} +1 -1
  23. phoenix/version.py +1 -1
  24. phoenix/server/static/assets/components-BYH03rjA.js +0 -1228
  25. phoenix/server/static/assets/vendor-aSQri0vz.js +0 -641
  26. phoenix/server/static/assets/vendor-codemirror-CYHkhs7D.js +0 -15
  27. {arize_phoenix-4.28.1.dist-info → arize_phoenix-4.30.0.dist-info}/WHEEL +0 -0
  28. {arize_phoenix-4.28.1.dist-info → arize_phoenix-4.30.0.dist-info}/licenses/IP_NOTICE +0 -0
  29. {arize_phoenix-4.28.1.dist-info → arize_phoenix-4.30.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.28.1
3
+ Version: 4.30.0
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -21,6 +21,7 @@ Requires-Dist: aioitertools
21
21
  Requires-Dist: aiosqlite
22
22
  Requires-Dist: alembic<2,>=1.3.0
23
23
  Requires-Dist: arize-phoenix-evals>=0.13.1
24
+ Requires-Dist: arize-phoenix-otel>=0.4.1
24
25
  Requires-Dist: cachetools
25
26
  Requires-Dist: fastapi
26
27
  Requires-Dist: grpcio
@@ -6,7 +6,7 @@ phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=OyML4t2XGnlqF0JXA9_uccL8HslTABxep9Ci7MViKEU,5216
8
8
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
9
- phoenix/version.py,sha256=Jx45Ra5pNxAdns6eybNAM7MCr8Ahz7F-CtwNB93XmFE,23
9
+ phoenix/version.py,sha256=Idu-4Zlthll3kvzYwrUdRM-Kz4cw2w_YQsMF2QSdL9Q,23
10
10
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
12
12
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -63,6 +63,9 @@ phoenix/metrics/mixins.py,sha256=moZ5hENIKzUQt2IRhWOd5EFXnoqQkVrpqEqMH7KQzyA,744
63
63
  phoenix/metrics/retrieval_metrics.py,sha256=XFQPo66h16w7-1AJ92M1VL_BUIXIWxXHGKF_QVOABZI,4384
64
64
  phoenix/metrics/timeseries.py,sha256=Cib3E0njJzi0vZpmyADvbakFQA98rIkfDaYAOmsmBz8,6277
65
65
  phoenix/metrics/wrappers.py,sha256=umZqa_5lf1wZSFe3FgzxF-qp1xbPdKD54W628GlGCUI,8392
66
+ phoenix/otel/__init__.py,sha256=YvEiD-3aGZs9agwLNCXU34ofV3G-Q-dolfsiinOJuT0,407
67
+ phoenix/otel/otel.py,sha256=pfFiqziSmTj8KepEZlVtjvSTd_SfNa6F8r9eqrOR6OQ,11829
68
+ phoenix/otel/settings.py,sha256=Qr2-RkgLQRfLhJqtLpEkSpqns7qLjPoOvpEOTqeSohM,3026
66
69
  phoenix/pointcloud/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
67
70
  phoenix/pointcloud/clustering.py,sha256=IzcG67kJ2hPP7pcqVmKPSL_6gKRonKdOT3bCtbTOqnk,820
68
71
  phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYGrao,2145
@@ -80,14 +83,15 @@ phoenix/server/thread_server.py,sha256=RwXQGP_QhGD7le6WB7xEygEEuwBl5Ck_Zo8xGIYGi
80
83
  phoenix/server/types.py,sha256=S2dReLNboR2nzjRK5j3MUyUDqu6AQFD7KRwJkeKj1q4,3609
81
84
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
85
  phoenix/server/api/context.py,sha256=WuhGT2549C5Yc7pWj2S7NaPeT4a-N-_mmz-Vg5bUkI8,3637
86
+ phoenix/server/api/exceptions.py,sha256=KdAzgwNan-wQ7THDrSoeJU2k9zWQVcH6lRiB462VsRA,990
83
87
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
84
- phoenix/server/api/queries.py,sha256=GXQzz1KmRqZCOENemdgoqBlTVA_4V2AZE-g0hBcO0Qk,23839
85
- phoenix/server/api/schema.py,sha256=BcxdqO5CSGqpKd-AAJHMjFlzaK9oJA8GJuxmMfcdjn4,434
88
+ phoenix/server/api/queries.py,sha256=hUzeHOUWuBQ-kjXh13-d5LgJfkbB8XSpFaHJX4YXpC8,23875
89
+ phoenix/server/api/schema.py,sha256=4L2m6QXhaV13YPTZCEZ3hqCPQFHZOy3QnJVLRYQFzpg,548
86
90
  phoenix/server/api/utils.py,sha256=Kl47G-1A7QKTDrc75BU2QK6HupsG6MWuXxy351FOfKQ,858
87
91
  phoenix/server/api/dataloaders/__init__.py,sha256=TrOGnU_SD_vEIxOE_dm8HrD5C2ScLFQ4xQ7f8r-E76s,3064
88
92
  phoenix/server/api/dataloaders/annotation_summaries.py,sha256=Wv8AORZoGd5TJ4Y-em8iqJu87AMpZP7lWOTr-SML-x8,5560
89
93
  phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=q091UmkXx37OBKh7L-GJ5LXHyRXfX2w4XTk1NMHtPpw,1827
90
- phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=i0g8F4akEf3kQOzAvBjO27QwXNsq-kJEM8dtzduxQgY,3720
94
+ phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=rZhJoIYUGgYhXwVBtq5u0bqtHmIQ2Sh6HNnJsSGIXis,3767
91
95
  phoenix/server/api/dataloaders/dataset_example_spans.py,sha256=-TjdyyJv2c2JiN1OXu6MMmQ-BEKlHXucEDcuObeRVsU,1416
92
96
  phoenix/server/api/dataloaders/document_evaluation_summaries.py,sha256=5XOom2KRAmCwPmtlraiZOSl3vhfaW-eiiYkmetAEalw,5616
93
97
  phoenix/server/api/dataloaders/document_evaluations.py,sha256=V6sE34jON_qFxt7eArJbktykAsty-gnBZHlEkORcj0E,1296
@@ -140,11 +144,11 @@ phoenix/server/api/input_types/TraceAnnotationSort.py,sha256=BzwiUnMh2VsgQYnhDlb
140
144
  phoenix/server/api/input_types/UserRoleInput.py,sha256=xxhFe0ITZOgRVEJbVem_W6F1Ip_H6xDENdQqMMx-kKE,129
141
145
  phoenix/server/api/input_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
146
  phoenix/server/api/mutations/__init__.py,sha256=JS3WRqYxNeoaLsKjODFvJnZb6CF19IFW-lfOsUq3rtM,1074
143
- phoenix/server/api/mutations/api_key_mutations.py,sha256=cv6AT6UAL55lTC_UqMdcN-1TjWAgqqZi__S9Tw12t6I,3688
147
+ phoenix/server/api/mutations/api_key_mutations.py,sha256=5GDsP0gURgXokbajc3GJEor3AGt6BGd1IMWMg054E7U,4683
144
148
  phoenix/server/api/mutations/auth.py,sha256=8o4tTfGCPkpUauuB9ijPH84Od77UX_UrQWfmUsnujI4,524
145
- phoenix/server/api/mutations/auth_mutations.py,sha256=6--KYnvY3k9JC70Z7iHWhrEa9xKjGH1sW-iOtC13Oxw,2041
146
- phoenix/server/api/mutations/dataset_mutations.py,sha256=0feBUW_07FEIx6uzepjxfRVhk5lAck0AkrqS1GVdoF4,27041
147
- phoenix/server/api/mutations/experiment_mutations.py,sha256=OXtLYdLA33RGy1MFctfv6ug2sODcDElhJph_J9vkIjk,3157
149
+ phoenix/server/api/mutations/auth_mutations.py,sha256=XLCxmsjyVp1riGWxUhVUOiGhIFs_ZmOfZM77vs_RLDw,2182
150
+ phoenix/server/api/mutations/dataset_mutations.py,sha256=8S6qjmraSBxA7ioNogTQPp6q27ZdvdAn6yt0Z4fmOI0,27096
151
+ phoenix/server/api/mutations/experiment_mutations.py,sha256=Z2xPrK8J117l5XWN-IvdKpykWIiVXysaUhzuwbIiLtk,3226
148
152
  phoenix/server/api/mutations/export_events_mutations.py,sha256=t_wYBxaqvBJYRoHslh3Bmoxmwlzoy0u8SsBKWIKN5hE,4028
149
153
  phoenix/server/api/mutations/project_mutations.py,sha256=MLm7I97lJ85hTuc1tq8sdYA8Ps5WKMV-bGqeeN-Ey90,2279
150
154
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=DM9gzxrMSAcxwXQ6jNaNGDVgl8oP50LZsBWRYQwLaSo,5955
@@ -160,7 +164,7 @@ phoenix/server/api/routers/v1/datasets.py,sha256=l3Hlc9AVyvX5GdT9iOXBsV-i4c_vtnC
160
164
  phoenix/server/api/routers/v1/evaluations.py,sha256=FSfz9MTi8s65F07abDXlb9-y97fDZSYbqsCXpimwO7g,12628
161
165
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=RTQnjupjmh07xowjq77ajbuAZhzIEfYxA4ZtECvGwOU,4844
162
166
  phoenix/server/api/routers/v1/experiment_runs.py,sha256=0G7GgGcZv9dzK47tsPp-p4k5O7W4F_aNRrsNuJN7mho,6393
163
- phoenix/server/api/routers/v1/experiments.py,sha256=3u275sGuYSiMyzC_obbjK3mf6aYb7SkY2c_wOg3z4xg,11751
167
+ phoenix/server/api/routers/v1/experiments.py,sha256=6Ouby3jQDZjeb_nJoD5eH8h2jxINBLrHzCPMsklzcJI,11820
164
168
  phoenix/server/api/routers/v1/pydantic_compat.py,sha256=FeK8oe2brqu-djsoqRxiKL4tw5cHmi89OHVfCFxYsAo,2890
165
169
  phoenix/server/api/routers/v1/spans.py,sha256=MAkMLrONFtItQxkHJde_Wpvz0jsgydegxVZOkZkRUsU,8781
166
170
  phoenix/server/api/routers/v1/traces.py,sha256=HJDmYKMATL40dZEJro6uQ3imbCZBzk3nUun9d21jcDs,7799
@@ -237,15 +241,15 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
237
241
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
238
242
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
239
243
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
240
- phoenix/server/static/.vite/manifest.json,sha256=1BcJFFm0NeLAAfZ6r47H6TP_44e5P36X2rORXCQE5e0,1929
241
- phoenix/server/static/assets/components-BYH03rjA.js,sha256=P4bMCzZcpwxqWV4NaUU4jpmay0GZusDgXbT9t9IUyeM,189918
242
- phoenix/server/static/assets/index-fqdjNpYm.js,sha256=SDvWOvsXmEJY3350kb2M0bRmC58xw7M3zDZ0MmdneeU,7515
243
- phoenix/server/static/assets/pages-DnbxgoTK.js,sha256=OLwZw5mVqnO6AelWT6ndr4kDOhf_8l6ezF5buwHBB7E,502087
244
+ phoenix/server/static/.vite/manifest.json,sha256=rkEVApCRSYofJpf7L3LFtAc9R6Yv7rijaaUCY2_AnXU,1929
245
+ phoenix/server/static/assets/components-CkSg5zK4.js,sha256=kMP22Fi58XDiVcsTUSkisv-SYM1E4y1R8Wp06r_pJNo,244317
246
+ phoenix/server/static/assets/index-DTecsU5w.js,sha256=qQbWg4v_Z385UAuIH30xlqhF3dFNhngnauRf3sOpj4o,7515
247
+ phoenix/server/static/assets/pages-C6emDFIO.js,sha256=5tmkYOe8IYM-CbQym_pAnkBv4SHEa1B1nNIqOqQ5Yzg,502735
248
+ phoenix/server/static/assets/vendor-DsnEJuEV.js,sha256=YyePucCaVO5Wke5vLm6NF6V3_rAomN7TqcBzv8QY6q0,1435199
244
249
  phoenix/server/static/assets/vendor-DxkFTwjz.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
245
- phoenix/server/static/assets/vendor-aSQri0vz.js,sha256=x_07SENutKMhtJ9HgFqkQHvwsDTfPkMmzQznY3HY7Zo,1359197
246
- phoenix/server/static/assets/vendor-arizeai-CsdcB1NH.js,sha256=VEn7hFJXcHV_DODmeDi9pEpF_D2NQ1bZYewbPe3BhIw,304008
247
- phoenix/server/static/assets/vendor-codemirror-CYHkhs7D.js,sha256=FmRfBdG2nCiQBDRc5bleOXwtUlepZjabWDnYhRq5rsc,503031
248
- phoenix/server/static/assets/vendor-recharts-B0sannek.js,sha256=Fi6Z_dwtI67mHHAdHISUHWzZQYsOwV1NGtomvOLv9xA,282859
250
+ phoenix/server/static/assets/vendor-arizeai-DtynTLNi.js,sha256=7D2zoxiUlFzqSpK-BDO4HlhbruUMmw3_CCDYQq6Mw-4,304008
251
+ phoenix/server/static/assets/vendor-codemirror-C5to5cK4.js,sha256=EYP0yvcRy8tInRIiGalhd5czgBYw_bmkLL9D_DDqAo0,516470
252
+ phoenix/server/static/assets/vendor-recharts-reihe2SJ.js,sha256=w13k19ajg2pNW1saRn3nKDMZil7RAB1vAg3eilYidsI,282859
249
253
  phoenix/server/static/assets/vendor-three-DwGkEfCM.js,sha256=0D12ZgKzfKCTSdSTKJBFR2RZO_xxeMXrqDp0AszZqHY,620972
250
254
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
251
255
  phoenix/server/templates/index.html,sha256=dAm0IClgJUdT5AOmjZvtgMg8F_xGrRGv95SAkUyx_kg,4325
@@ -291,8 +295,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
291
295
  phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
292
296
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
293
297
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
294
- arize_phoenix-4.28.1.dist-info/METADATA,sha256=YRglZu4_XOWN3jc_U8mD8QkVN5m1t6me2zuhNiw5iFk,11936
295
- arize_phoenix-4.28.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
296
- arize_phoenix-4.28.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
297
- arize_phoenix-4.28.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
298
- arize_phoenix-4.28.1.dist-info/RECORD,,
298
+ arize_phoenix-4.30.0.dist-info/METADATA,sha256=go0ab8700SkvxCH0PrtM8ZCkkvJ5-VOQpsMhMLCHk0k,11977
299
+ arize_phoenix-4.30.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
300
+ arize_phoenix-4.30.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
301
+ arize_phoenix-4.30.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
302
+ arize_phoenix-4.30.0.dist-info/RECORD,,
@@ -0,0 +1,22 @@
1
+ from opentelemetry.sdk.resources import Resource
2
+
3
+ from .otel import (
4
+ PROJECT_NAME,
5
+ BatchSpanProcessor,
6
+ GRPCSpanExporter,
7
+ HTTPSpanExporter,
8
+ SimpleSpanProcessor,
9
+ TracerProvider,
10
+ register,
11
+ )
12
+
13
+ __all__ = [
14
+ "TracerProvider",
15
+ "SimpleSpanProcessor",
16
+ "BatchSpanProcessor",
17
+ "HTTPSpanExporter",
18
+ "GRPCSpanExporter",
19
+ "Resource",
20
+ "PROJECT_NAME",
21
+ "register",
22
+ ]
phoenix/otel/otel.py ADDED
@@ -0,0 +1,284 @@
1
+ import inspect
2
+ import os
3
+ import warnings
4
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast
5
+ from urllib.parse import ParseResult, urlparse
6
+
7
+ from openinference.semconv.resource import ResourceAttributes as _ResourceAttributes
8
+ from opentelemetry import trace as trace_api
9
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
10
+ OTLPSpanExporter as _GRPCSpanExporter,
11
+ )
12
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
13
+ OTLPSpanExporter as _HTTPSpanExporter,
14
+ )
15
+ from opentelemetry.sdk.resources import Resource
16
+ from opentelemetry.sdk.trace import SpanProcessor
17
+ from opentelemetry.sdk.trace import TracerProvider as _TracerProvider
18
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor as _BatchSpanProcessor
19
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor as _SimpleSpanProcessor
20
+ from opentelemetry.sdk.trace.export import SpanExporter
21
+
22
+ from .settings import get_env_client_headers, get_env_collector_endpoint, get_env_project_name
23
+
24
+ PROJECT_NAME = _ResourceAttributes.PROJECT_NAME
25
+
26
+ _DEFAULT_GRPC_PORT = 4317
27
+
28
+
29
+ def register(
30
+ *,
31
+ endpoint: Optional[str] = None,
32
+ project_name: Optional[str] = None,
33
+ batch: bool = False,
34
+ set_global_tracer_provider: bool = True,
35
+ headers: Optional[Dict[str, str]] = None,
36
+ verbose: bool = True,
37
+ ) -> _TracerProvider:
38
+ """
39
+ Creates an OpenTelemetry TracerProvider for enabling OpenInference tracing.
40
+
41
+ For futher configuration, the `phoenix.otel` module provides drop-in replacements for
42
+ OpenTelemetry TracerProvider, SimpleSpanProcessor, BatchSpanProcessor, HTTPSpanExporter, and
43
+ GRPCSpanExporter objects with Phoenix-aware defaults. Documentation on how to configure tracing
44
+ can be found at https://opentelemetry.io/docs/specs/otel/trace/sdk/.
45
+
46
+ Args:
47
+ endpoint (str, optional): The collector endpoint to which spans will be exported. If not
48
+ provided, the `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be used. The
49
+ export protocol will be inferred from the endpoint.
50
+ project_name (str, optional): The name of the project to which spans will be associated. If
51
+ not provided, the `PHOENIX_PROJECT_NAME` environment variable will be used.
52
+ batch (bool): If True, spans will be processed using a BatchSpanprocessor. If False, spans
53
+ will be processed one at a time using a SimpleSpanProcessor.
54
+ set_global_tracer_provider (bool): If False, the TracerProvider will not be set as the global
55
+ tracer provider. Defaults to True.
56
+ headers (dict, optional): Optional headers to include in the HTTP request to the collector.
57
+ verbose (bool): If True, configuration details will be printed to stdout.
58
+ """
59
+
60
+ project_name = project_name or get_env_project_name()
61
+ resource = Resource.create({PROJECT_NAME: project_name})
62
+ tracer_provider = TracerProvider(resource=resource, verbose=False)
63
+ span_processor: SpanProcessor
64
+ if batch:
65
+ span_processor = BatchSpanProcessor(endpoint=endpoint, headers=headers)
66
+ else:
67
+ span_processor = SimpleSpanProcessor(endpoint=endpoint, headers=headers)
68
+ tracer_provider.add_span_processor(span_processor)
69
+ tracer_provider._default_processor = True
70
+
71
+ if set_global_tracer_provider:
72
+ trace_api.set_tracer_provider(tracer_provider)
73
+ global_provider_msg = (
74
+ "| \n"
75
+ "| `register` has set this TracerProvider as the global OpenTelemetry default.\n"
76
+ "| To disable this behavior, call `register` with `set_global_tracer_provider=False`.\n"
77
+ )
78
+ else:
79
+ global_provider_msg = ""
80
+
81
+ details = tracer_provider._tracing_details()
82
+ if verbose:
83
+ print(f"{details}" f"{global_provider_msg}")
84
+ return tracer_provider
85
+
86
+
87
+ class TracerProvider(_TracerProvider):
88
+ def __init__(
89
+ self, *args: Any, endpoint: Optional[str] = None, verbose: bool = True, **kwargs: Any
90
+ ):
91
+ sig = inspect.signature(_TracerProvider)
92
+ bound_args = sig.bind_partial(*args, **kwargs)
93
+ bound_args.apply_defaults()
94
+ if bound_args.arguments.get("resource") is None:
95
+ bound_args.arguments["resource"] = Resource.create(
96
+ {PROJECT_NAME: get_env_project_name()}
97
+ )
98
+ super().__init__(**bound_args.arguments)
99
+
100
+ parsed_url, endpoint = _normalized_endpoint(endpoint)
101
+ self._default_processor = False
102
+
103
+ if _maybe_http_endpoint(parsed_url):
104
+ http_exporter: SpanExporter = HTTPSpanExporter(endpoint=endpoint)
105
+ self.add_span_processor(SimpleSpanProcessor(span_exporter=http_exporter))
106
+ self._default_processor = True
107
+ elif _maybe_grpc_endpoint(parsed_url):
108
+ grpc_exporter: SpanExporter = GRPCSpanExporter(endpoint=endpoint)
109
+ self.add_span_processor(SimpleSpanProcessor(span_exporter=grpc_exporter))
110
+ self._default_processor = True
111
+ if verbose:
112
+ print(self._tracing_details())
113
+
114
+ def add_span_processor(self, *args: Any, **kwargs: Any) -> None:
115
+ if self._default_processor:
116
+ self._active_span_processor.shutdown()
117
+ self._active_span_processor._span_processors = tuple() # remove default processors
118
+ self._default_processor = False
119
+ return super().add_span_processor(*args, **kwargs)
120
+
121
+ def _tracing_details(self) -> str:
122
+ project = self.resource.attributes.get(PROJECT_NAME)
123
+ processor_name: Optional[str] = None
124
+ endpoint: Optional[str] = None
125
+ transport: Optional[str] = None
126
+ headers: Optional[Union[Dict[str, str], str]] = None
127
+
128
+ if self._active_span_processor:
129
+ if processors := self._active_span_processor._span_processors:
130
+ if len(processors) == 1:
131
+ span_processor = self._active_span_processor._span_processors[0]
132
+ if exporter := getattr(span_processor, "span_exporter"):
133
+ processor_name = span_processor.__class__.__name__
134
+ endpoint = exporter._endpoint
135
+ transport = _exporter_transport(exporter)
136
+ headers = _printable_headers(exporter._headers)
137
+ else:
138
+ processor_name = "Multiple Span Processors"
139
+ endpoint = "Multiple Span Exporters"
140
+ transport = "Multiple Span Exporters"
141
+ headers = "Multiple Span Exporters"
142
+
143
+ if os.name == "nt":
144
+ details_header = "OpenTelemetry Tracing Details"
145
+ else:
146
+ details_header = "🔭 OpenTelemetry Tracing Details 🔭"
147
+
148
+ configuration_msg = (
149
+ "| Using a default SpanProcessor. `add_span_processor` will overwrite this default.\n"
150
+ )
151
+
152
+ details_msg = (
153
+ f"{details_header}\n"
154
+ f"| Phoenix Project: {project}\n"
155
+ f"| Span Processor: {processor_name}\n"
156
+ f"| Collector Endpoint: {endpoint}\n"
157
+ f"| Transport: {transport}\n"
158
+ f"| Transport Headers: {headers}\n"
159
+ "| \n"
160
+ f"{configuration_msg if self._default_processor else ''}"
161
+ )
162
+ return details_msg
163
+
164
+
165
+ class SimpleSpanProcessor(_SimpleSpanProcessor):
166
+ def __init__(
167
+ self,
168
+ span_exporter: Optional[SpanExporter] = None,
169
+ endpoint: Optional[str] = None,
170
+ headers: Optional[Dict[str, str]] = None,
171
+ ):
172
+ if span_exporter is None:
173
+ parsed_url, endpoint = _normalized_endpoint(endpoint)
174
+ if _maybe_http_endpoint(parsed_url):
175
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
176
+ elif _maybe_grpc_endpoint(parsed_url):
177
+ span_exporter = GRPCSpanExporter(endpoint=endpoint, headers=headers)
178
+ else:
179
+ warnings.warn("Could not infer collector endpoint protocol, defaulting to HTTP.")
180
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
181
+ super().__init__(span_exporter)
182
+
183
+
184
+ class BatchSpanProcessor(_BatchSpanProcessor):
185
+ def __init__(
186
+ self,
187
+ span_exporter: Optional[SpanExporter] = None,
188
+ endpoint: Optional[str] = None,
189
+ headers: Optional[Dict[str, str]] = None,
190
+ ):
191
+ if span_exporter is None:
192
+ parsed_url, endpoint = _normalized_endpoint(endpoint)
193
+ if _maybe_http_endpoint(parsed_url):
194
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
195
+ elif _maybe_grpc_endpoint(parsed_url):
196
+ span_exporter = GRPCSpanExporter(endpoint=endpoint, headers=headers)
197
+ else:
198
+ warnings.warn("Could not infer collector endpoint protocol, defaulting to HTTP.")
199
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
200
+ super().__init__(span_exporter)
201
+
202
+
203
+ class HTTPSpanExporter(_HTTPSpanExporter):
204
+ def __init__(self, *args: Any, **kwargs: Any):
205
+ sig = inspect.signature(_HTTPSpanExporter)
206
+ bound_args = sig.bind_partial(*args, **kwargs)
207
+ bound_args.apply_defaults()
208
+
209
+ if not bound_args.arguments.get("headers"):
210
+ bound_args.arguments["headers"] = get_env_client_headers()
211
+
212
+ if bound_args.arguments.get("endpoint") is None:
213
+ _, endpoint = _normalized_endpoint(None)
214
+ bound_args.arguments["endpoint"] = endpoint
215
+ super().__init__(**bound_args.arguments)
216
+
217
+
218
+ class GRPCSpanExporter(_GRPCSpanExporter):
219
+ def __init__(self, *args: Any, **kwargs: Any):
220
+ sig = inspect.signature(_GRPCSpanExporter)
221
+ bound_args = sig.bind_partial(*args, **kwargs)
222
+ bound_args.apply_defaults()
223
+
224
+ if not bound_args.arguments.get("headers"):
225
+ bound_args.arguments["headers"] = get_env_client_headers()
226
+
227
+ if bound_args.arguments.get("endpoint") is None:
228
+ _, endpoint = _normalized_endpoint(None)
229
+ bound_args.arguments["endpoint"] = endpoint
230
+ super().__init__(**bound_args.arguments)
231
+
232
+
233
+ def _maybe_http_endpoint(parsed_endpoint: ParseResult) -> bool:
234
+ if parsed_endpoint.path == "/v1/traces":
235
+ return True
236
+ return False
237
+
238
+
239
+ def _maybe_grpc_endpoint(parsed_endpoint: ParseResult) -> bool:
240
+ if not parsed_endpoint.path and parsed_endpoint.port == 4317:
241
+ return True
242
+ return False
243
+
244
+
245
+ def _exporter_transport(exporter: SpanExporter) -> str:
246
+ if isinstance(exporter, _HTTPSpanExporter):
247
+ return "HTTP"
248
+ if isinstance(exporter, _GRPCSpanExporter):
249
+ return "gRPC"
250
+ else:
251
+ return exporter.__class__.__name__
252
+
253
+
254
+ def _printable_headers(headers: Union[List[Tuple[str, str]], Dict[str, str]]) -> Dict[str, str]:
255
+ if isinstance(headers, dict):
256
+ return {key.lower(): "****" for key, _ in headers.items()}
257
+ return {key.lower(): "****" for key, _ in headers}
258
+
259
+
260
+ def _construct_http_endpoint(parsed_endpoint: ParseResult) -> ParseResult:
261
+ return parsed_endpoint._replace(path="/v1/traces")
262
+
263
+
264
+ def _construct_grpc_endpoint(parsed_endpoint: ParseResult) -> ParseResult:
265
+ return parsed_endpoint._replace(netloc=f"{parsed_endpoint.hostname}:{_DEFAULT_GRPC_PORT}")
266
+
267
+
268
+ _KNOWN_PROVIDERS = {
269
+ "app.phoenix.arize.com": _construct_http_endpoint,
270
+ }
271
+
272
+
273
+ def _normalized_endpoint(endpoint: Optional[str]) -> Tuple[ParseResult, str]:
274
+ if endpoint is None:
275
+ base_endpoint = get_env_collector_endpoint() or "http://localhost:6006"
276
+ parsed = urlparse(base_endpoint)
277
+ if parsed.hostname in _KNOWN_PROVIDERS:
278
+ parsed = _KNOWN_PROVIDERS[parsed.hostname](parsed)
279
+ else:
280
+ parsed = _construct_grpc_endpoint(parsed)
281
+ else:
282
+ parsed = urlparse(endpoint)
283
+ parsed = cast(ParseResult, parsed)
284
+ return parsed, parsed.geturl()
@@ -0,0 +1,82 @@
1
+ import os
2
+ import urllib
3
+ from logging import getLogger
4
+ from re import compile
5
+ from typing import Dict, List, Optional
6
+
7
+ _logger = getLogger(__name__)
8
+
9
+ # Environment variables specific to the subpackage
10
+ ENV_PHOENIX_COLLECTOR_ENDPOINT = "PHOENIX_COLLECTOR_ENDPOINT"
11
+ ENV_PHOENIX_PROJECT_NAME = "PHOENIX_PROJECT_NAME"
12
+ ENV_PHOENIX_CLIENT_HEADERS = "PHOENIX_CLIENT_HEADERS"
13
+
14
+
15
+ def get_env_collector_endpoint() -> Optional[str]:
16
+ return os.getenv(ENV_PHOENIX_COLLECTOR_ENDPOINT)
17
+
18
+
19
+ def get_env_project_name() -> str:
20
+ return os.getenv(ENV_PHOENIX_PROJECT_NAME, "default")
21
+
22
+
23
+ def get_env_client_headers() -> Optional[Dict[str, str]]:
24
+ if headers_str := os.getenv(ENV_PHOENIX_CLIENT_HEADERS):
25
+ return parse_env_headers(headers_str)
26
+ return None
27
+
28
+
29
+ # Optional whitespace
30
+ _OWS = r"[ \t]*"
31
+ # A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{}
32
+ _KEY_FORMAT = r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+"
33
+ # A value contains a URL-encoded UTF-8 string. The encoded form can contain any
34
+ # printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/
35
+ _VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
36
+ # A key-value is key=value, with optional whitespace surrounding key and value
37
+ _KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}"
38
+
39
+ _HEADER_PATTERN = compile(_KEY_VALUE_FORMAT)
40
+ _DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*")
41
+
42
+
43
+ def parse_env_headers(s: str) -> Dict[str, str]:
44
+ """
45
+ Parse ``s``, which is a ``str`` instance containing HTTP headers encoded
46
+ for use in ENV variables per the W3C Baggage HTTP header format at
47
+ https://www.w3.org/TR/baggage/#baggage-http-header-format, except that
48
+ additional semi-colon delimited metadata is not supported.
49
+
50
+ If the headers are not urlencoded, we will log a warning and attempt to urldecode them.
51
+ """
52
+ headers: Dict[str, str] = {}
53
+ headers_list: List[str] = _DELIMITER_PATTERN.split(s)
54
+
55
+ for header in headers_list:
56
+ if not header: # empty string
57
+ continue
58
+
59
+ match = _HEADER_PATTERN.fullmatch(header.strip())
60
+ if not match:
61
+ parts = header.split("=", 1)
62
+ name, value = parts
63
+ encoded_header = f"{urllib.parse.quote(name)}={urllib.parse.quote(value)}"
64
+ match = _HEADER_PATTERN.fullmatch(encoded_header.strip())
65
+ if not match:
66
+ _logger.warning(
67
+ "Header format invalid! Header values in environment variables must be "
68
+ "URL encoded: %s",
69
+ f"{name}: ****",
70
+ )
71
+ continue
72
+ _logger.warning(
73
+ "Header values in environment variables should be URL encoded, attempting to "
74
+ "URL encode header: {name}: ****"
75
+ )
76
+
77
+ name, value = header.split("=", 1)
78
+ name = urllib.parse.unquote(name).strip().lower()
79
+ value = urllib.parse.unquote(value).strip()
80
+ headers[name] = value
81
+
82
+ return headers
@@ -10,6 +10,7 @@ from strawberry.dataloader import DataLoader
10
10
  from typing_extensions import TypeAlias
11
11
 
12
12
  from phoenix.db import models
13
+ from phoenix.server.api.exceptions import NotFound
13
14
  from phoenix.server.api.types.DatasetExampleRevision import DatasetExampleRevision
14
15
  from phoenix.server.types import DbSessionFactory
15
16
 
@@ -24,7 +25,7 @@ class DatasetExampleRevisionsDataLoader(DataLoader[Key, Result]):
24
25
  super().__init__(load_fn=self._load_fn)
25
26
  self._db = db
26
27
 
27
- async def _load_fn(self, keys: List[Key]) -> List[Union[Result, ValueError]]:
28
+ async def _load_fn(self, keys: List[Key]) -> List[Union[Result, NotFound]]:
28
29
  # sqlalchemy has limited SQLite support for VALUES, so use UNION ALL instead.
29
30
  # For details, see https://github.com/sqlalchemy/sqlalchemy/issues/7228
30
31
  keys_subquery = union(
@@ -95,4 +96,4 @@ class DatasetExampleRevisionsDataLoader(DataLoader[Key, Result]):
95
96
  ) in await session.stream(query)
96
97
  if is_valid_version
97
98
  }
98
- return [results.get(key, ValueError("Could not find revision.")) for key in keys]
99
+ return [results.get(key, NotFound("Could not find revision.")) for key in keys]
@@ -0,0 +1,41 @@
1
+ from graphql.error import GraphQLError
2
+ from strawberry.extensions import MaskErrors
3
+
4
+
5
+ class CustomGraphQLError(Exception):
6
+ """
7
+ An error that represents an expected error scenario in a GraphQL resolver.
8
+ """
9
+
10
+
11
+ class BadRequest(CustomGraphQLError):
12
+ """
13
+ An error raised due to a malformed or invalid request.
14
+ """
15
+
16
+
17
+ class NotFound(CustomGraphQLError):
18
+ """
19
+ An error raised when the requested resource is not found.
20
+ """
21
+
22
+
23
+ class Unauthorized(CustomGraphQLError):
24
+ """
25
+ An error raised when login fails or a user or other entity is not authorized
26
+ to access a resource.
27
+ """
28
+
29
+
30
+ def get_mask_errors_extension() -> MaskErrors:
31
+ return MaskErrors(
32
+ should_mask_error=_should_mask_error,
33
+ error_message="an unexpected error occurred",
34
+ )
35
+
36
+
37
+ def _should_mask_error(error: GraphQLError) -> bool:
38
+ """
39
+ Masks unexpected errors raised from GraphQL resolvers.
40
+ """
41
+ return not isinstance(error.original_error, CustomGraphQLError)
@@ -5,12 +5,15 @@ import jwt
5
5
  import strawberry
6
6
  from sqlalchemy import insert, select
7
7
  from strawberry import UNSET
8
+ from strawberry.relay import GlobalID
8
9
  from strawberry.types import Info
9
10
 
10
11
  from phoenix.db import models
11
12
  from phoenix.server.api.context import Context
13
+ from phoenix.server.api.exceptions import NotFound
12
14
  from phoenix.server.api.mutations.auth import HasSecret, IsAuthenticated
13
15
  from phoenix.server.api.queries import Query
16
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
14
17
  from phoenix.server.api.types.SystemApiKey import SystemApiKey
15
18
 
16
19
 
@@ -28,6 +31,16 @@ class CreateApiKeyInput:
28
31
  expires_at: Optional[datetime] = UNSET
29
32
 
30
33
 
34
+ @strawberry.input
35
+ class DeleteApiKeyInput:
36
+ id: GlobalID
37
+
38
+
39
+ @strawberry.type
40
+ class DeleteSystemApiKeyMutationPayload:
41
+ id: GlobalID
42
+
43
+
31
44
  @strawberry.type
32
45
  class ApiKeyMutationMixin:
33
46
  @strawberry.mutation(permission_classes=[HasSecret, IsAuthenticated]) # type: ignore
@@ -79,6 +92,22 @@ class ApiKeyMutationMixin:
79
92
  query=Query(),
80
93
  )
81
94
 
95
+ @strawberry.mutation(permission_classes=[HasSecret, IsAuthenticated]) # type: ignore
96
+ async def delete_system_api_key(
97
+ self, info: Info[Context, None], input: DeleteApiKeyInput
98
+ ) -> DeleteSystemApiKeyMutationPayload:
99
+ api_key_id = from_global_id_with_expected_type(
100
+ input.id, expected_type_name=SystemApiKey.__name__
101
+ )
102
+ async with info.context.db() as session:
103
+ api_key = await session.get(models.APIKey, api_key_id)
104
+ if api_key is None:
105
+ raise NotFound(f"Unknown System API Key: {input.id}")
106
+
107
+ await session.delete(api_key)
108
+
109
+ return DeleteSystemApiKeyMutationPayload(id=input.id)
110
+
82
111
 
83
112
  def create_jwt(
84
113
  *,
@@ -8,10 +8,12 @@ from strawberry.types import Info
8
8
  from phoenix.auth import is_valid_password
9
9
  from phoenix.db import models
10
10
  from phoenix.server.api.context import Context
11
+ from phoenix.server.api.exceptions import Unauthorized
11
12
  from phoenix.server.api.mutations.auth import HasSecret
12
13
 
13
14
  PHOENIX_ACCESS_TOKEN_COOKIE_NAME = "phoenix-access-token"
14
15
  PHOENIX_ACCESS_TOKEN_COOKIE_MAX_AGE_IN_SECONDS = int(timedelta(days=31).total_seconds())
16
+ FAILED_LOGIN_MESSAGE = "login failed"
15
17
 
16
18
 
17
19
  @strawberry.input
@@ -34,7 +36,7 @@ class AuthMutationMixin:
34
36
  select(models.User).where(models.User.email == input.email)
35
37
  )
36
38
  ) is None or (password_hash := user.password_hash) is None:
37
- raise ValueError
39
+ raise Unauthorized(FAILED_LOGIN_MESSAGE)
38
40
  secret = info.context.get_secret()
39
41
  loop = asyncio.get_running_loop()
40
42
  if not await loop.run_in_executor(
@@ -43,7 +45,7 @@ class AuthMutationMixin:
43
45
  password=input.password, salt=secret, password_hash=password_hash
44
46
  ),
45
47
  ):
46
- raise ValueError
48
+ raise Unauthorized(FAILED_LOGIN_MESSAGE)
47
49
  response = info.context.get_response()
48
50
  response.set_cookie(
49
51
  key=PHOENIX_ACCESS_TOKEN_COOKIE_NAME,